# TODO eliminate the annotation
import json
from functools import reduce

import requests
from flask_restful import Resource, reqparse
from kubernetes import config, client
from kubernetes.client.models import V1Deployment
from mongoengine import DoesNotExist

from app.resources.helpers import *
from app.models import Instance as DeploymentDocument

config.load_incluster_config()


class Deployment(Resource):
    parser = reqparse.RequestParser()
    parser.add_argument('Authorization', location='headers')
    parser.add_argument('image')
    parser.add_argument('name')
    parser.add_argument('replicas')
    parser.add_argument('env', type=dict)
    parser.add_argument('ports', type=list, location='json')
    # for publish, not test
    parser.add_argument('publishStatus', type=bool)
    apps_v1_api = client.AppsV1Api()
    core_v1_api = client.CoreV1Api()
    api_client = client.ApiClient()

    @staticmethod
    def __customize_deployment_item(item: V1Deployment) -> dict:
        namespace = item.metadata.namespace
        name = item.metadata.name
        deployment_obj = DeploymentDocument.objects(
            namespace=namespace, name=name
        )[0]
        return {
            'replicas':
                item.status.replicas,
            'instanceId': deployment_obj.instance_id,
            'availableReplicas':
                item.status.available_replicas,
            'name':
                name,
            'creationTimestamp':
                item.metadata.creation_timestamp.isoformat(),
            'conditions':
                Deployment.api_client.sanitize_for_serialization(
                    item.status.conditions),
            'publishStatus':
                deployment_obj.publish_status,
            'image':
                deployment_obj.service,
            'env': reduce(lambda x, y: {**x, (y.name): y.value},
                          item.spec.template.spec.containers[0].env or [], {})
            # 'env': list(map(lambda x: {'name': x.name, 'value': x.value},
            # item.spec.template.spec.containers[0].env))
        }

    @staticmethod
    def __gen_deployment_model(namespace, name, service,
                               device_number,
                               port):
        return DeploymentDocument(namespace=namespace,
                                  name=name,
                                  service=service,
                                  publish_status=False,
                                  device_number=device_number,
                                  port=port,
                                  instance_id=f'http://{name}.{namespace}:{port}')

    # /deployment/other mine
    def get(self, suffix=None):
        data = Deployment.parser.parse_args()
        namespace = get_source_from_data(data)
        if not suffix or suffix not in ['mine', 'other']:
            return result_error('404', 'not found'), 404
        if not namespace:
            return result_error('400', 'no namespace specified'), 400
        if suffix == 'mine':
            ret = Deployment.apps_v1_api.list_namespaced_deployment(
                namespace=namespace, watch=False)
            return result_success(
                list(map(Deployment.__customize_deployment_item, ret.items)))
        else:
            # not equal to this namespace and remove _id
            documents = DeploymentDocument \
                .objects(namespace__ne=namespace, publish_status=True) \
                .exclude('id')
            return result_success(
                json.loads(documents.to_json())
            )

    # Create deployment & service in k8s. Single container pod & stateless app
    #  for now. Note that the protocol of the ports for the service is TCP by
    #  default.
    def post(self):
        data = Deployment.parser.parse_args()
        name = data['name']
        image = data['image']
        namespace = get_source_from_data(data)
        ports = data['ports'] or []
        replicas = int(data['replicas'] or 1)
        env = data['env'] or {}
        device_number = env.get('DEVICE_NUMBER', 'undefined')
        # validate input
        if not (name and image and namespace):
            return result_error('400',
                                'no name or image or namespace specified')
        # create namespace if not exists
        namespaces = Deployment.core_v1_api.list_namespace(
            field_selector='metadata.name=' + namespace)
        if len(namespaces.items) == 0:
            namespace_object = client.V1Namespace(
                api_version='v1',
                kind='Namespace',
                metadata=client.V1ObjectMeta(name=namespace))
            Deployment.core_v1_api.create_namespace(body=namespace_object)
        # Set environment variables for the container
        env_list = [client.V1EnvVar(name=key, value=env[key]) for key in env]
        # Configurate Pod template container
        container = client.V1Container(
            name=name,
            image=image,
            image_pull_policy='IfNotPresent',
            ports=[client.V1ContainerPort(container_port=i) for i in ports],
            env=env_list)
        # Create and configurate a spec section
        template = client.V1PodTemplateSpec(
            metadata=client.V1ObjectMeta(labels={'app': name}),
            spec=client.V1PodSpec(containers=[container]))
        # Create the specification of deployment
        deployment_spec = client.V1DeploymentSpec(
            replicas=replicas,
            template=template,
            selector={'matchLabels': {
                'app': name
            }})
        # Instantiate the deployment object
        deployment = client.V1Deployment(
            api_version='apps/v1',
            kind='Deployment',
            metadata=client.V1ObjectMeta(name=name),
            spec=deployment_spec)
        # Create the specification of service
        service_spec = client.V1ServiceSpec(
            selector={'app': name},
            ports=[client.V1ServicePort(port=i) for i in ports])
        # Instantiate the service object
        service = client.V1Service(
            api_version='v1',
            kind='Service',
            metadata=client.V1ObjectMeta(name=name),
            spec=service_spec,
        )

        # Create the deployment & service
        Deployment.core_v1_api.create_namespaced_service(body=service,
                                                         namespace=namespace)
        # return the api response for deployment creation only
        ret = Deployment.apps_v1_api.create_namespaced_deployment(
            body=deployment, namespace=namespace)
        # save data to mongo
        document = Deployment. \
            __gen_deployment_model(namespace, name, image, device_number,
                                   # get first port or 8080 by default
                                   ports[0] if len(ports) > 0 else 8080)
        document.save()
        return result_success(
            Deployment.api_client.sanitize_for_serialization(ret))

    # placeholder for creating stateful deployment & service
    def __stateful_deployment(self):
        pass

    def delete(self):
        data = Deployment.parser.parse_args()
        name = data['name']
        namespace = get_source_from_data(data)
        if not (name and namespace):
            return result_error('400',
                                'no deployment name or namespace specified')
        body = client.V1DeleteOptions()
        Deployment.core_v1_api.delete_namespaced_service(name,
                                                         namespace,
                                                         body=body)
        # return the api response for deployment deletion only
        ret = Deployment.apps_v1_api.delete_namespaced_deployment(
            name, namespace, pretty='true', body=body)
        # delete the data in mongo
        instance_id = DeploymentDocument.objects(
            namespace=namespace,
            name=name
        )[0].instance_id
        DeploymentDocument.objects(namespace=namespace, name=name).delete()
        # delete relationship of ecn & user
        try:
            requests.delete('http://process-management:9000/operator/relation',
                            params={'instanceId': instance_id})
        except Exception:
            pass
        return result_success(
            Deployment.api_client.sanitize_for_serialization(ret)
        )

    # change the publish status
    def put(self):
        data = Deployment.parser.parse_args()
        # new status from request
        publish_status = data['publishStatus']
        name = data['name']
        namespace = get_source_from_data(data)
        try:
            deployment = DeploymentDocument \
                .objects(name=name, namespace=namespace) \
                .get()
            deployment.update(publish_status=publish_status)
            return result_success(None)
        except DoesNotExist:
            return result_error('404', 'deployment not found'), 404
